系列文章: 前端工程師的 Modern Web 實踐之道 - Day 11
預計閱讀時間: 12 分鐘
難度等級: ⭐⭐⭐⭐☆
在前一篇文章中,我們探討了狀態管理的不同方案選擇。今天我們將深入探討前端開發中另一個核心課題:API 設計與前端整合。這個主題將幫助你從全端視角理解如何設計高效能、易維護的 API,並在前端實作最佳的資料處理策略。
還在為過度請求(Over-fetching)或請求不足(Under-fetching)而煩惱嗎?每次新增一個頁面就要請後端加一個新 API?今天我們來聊聊如何徹底解決這些問題。
在現代 Web 應用中,前後端分離已經成為標準架構。但這也帶來新的挑戰:
傳統 RESTful API 的痛點:
// 取得使用者資訊需要多次請求
const user = await fetch('/api/users/123');
const posts = await fetch('/api/users/123/posts');
const comments = await fetch('/api/users/123/comments');
const followers = await fetch('/api/users/123/followers');
// 4 次請求,4 次往返,大量時間浪費
// 更糟的是:可能拿到很多用不到的資料
GraphQL 的承諾:
// 一次請求拿到所有需要的資料
const data = await graphql(`
query {
user(id: "123") {
name
email
posts(limit: 10) {
title
createdAt
}
followers(limit: 5) {
name
}
}
}
`);
// 只拿需要的欄位,一次請求完成
但 GraphQL 真的是銀彈嗎?讓我們深入分析。
核心特性:
適用場景:
核心特性:
適用場景:
讓我分享一個真實案例:
專案背景:電商平台重構
- 桌面版、行動版、小程式三個客戶端
- 商品詳情頁需要展示:基本資訊、規格、評論、推薦、優惠券等
- 不同平台展示的資料結構差異很大
最初方案 (RESTful):
GET /api/products/123
GET /api/products/123/specs
GET /api/products/123/reviews
GET /api/products/123/recommendations
GET /api/products/123/coupons
問題:
- 行動版只需要部分資料,卻要請求所有 API
- 桌面版需要更多細節,又要加新的 API
- 小程式有特殊需求,API 越來越多
改用 GraphQL 後:
// 行動版 - 只請求基本資訊
query MobileProduct {
product(id: "123") {
name
price
mainImage
reviews(limit: 3) { rating }
}
}
// 桌面版 - 請求完整資訊
query DesktopProduct {
product(id: "123") {
name
price
description
images
specs { ... }
reviews(limit: 20) { ... }
recommendations { ... }
}
}
效果:
- API 請求數減少 60%
- 資料傳輸量減少 40%
- 開發新頁面時間減少 50%
讓我們打造一個具備完整錯誤處理、快取機制和請求攔截的 API 客戶端:
/**
* 現代化 RESTful API 客戶端
* 特性:型別安全、自動重試、請求快取、錯誤處理
*/
interface ApiClientConfig {
baseURL: string;
timeout?: number;
retryAttempts?: number;
cacheTime?: number;
headers?: Record<string, string>;
}
interface RequestConfig {
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
url: string;
data?: any;
params?: Record<string, any>;
cache?: boolean;
}
class ApiClient {
private config: Required<ApiClientConfig>;
private cache: Map<string, { data: any; timestamp: number }>;
private requestInterceptors: Array<(config: RequestConfig) => RequestConfig> = [];
private responseInterceptors: Array<(response: any) => any> = [];
constructor(config: ApiClientConfig) {
this.config = {
timeout: 10000,
retryAttempts: 3,
cacheTime: 5 * 60 * 1000, // 5 分鐘
headers: {},
...config,
};
this.cache = new Map();
}
/**
* 添加請求攔截器
* 用途:統一添加認證 token、追蹤請求等
*/
addRequestInterceptor(interceptor: (config: RequestConfig) => RequestConfig) {
this.requestInterceptors.push(interceptor);
}
/**
* 添加回應攔截器
* 用途:統一錯誤處理、資料轉換等
*/
addResponseInterceptor(interceptor: (response: any) => any) {
this.responseInterceptors.push(interceptor);
}
/**
* 生成快取鍵值
*/
private getCacheKey(config: RequestConfig): string {
const { method, url, params } = config;
return `${method}:${url}:${JSON.stringify(params || {})}`;
}
/**
* 檢查快取
*/
private getCache(key: string): any | null {
const cached = this.cache.get(key);
if (!cached) return null;
const isExpired = Date.now() - cached.timestamp > this.config.cacheTime;
if (isExpired) {
this.cache.delete(key);
return null;
}
return cached.data;
}
/**
* 設定快取
*/
private setCache(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
});
}
/**
* 執行請求(含重試機制)
*/
private async executeRequest(
config: RequestConfig,
attempt: number = 1
): Promise<any> {
try {
// 套用請求攔截器
let finalConfig = { ...config };
for (const interceptor of this.requestInterceptors) {
finalConfig = interceptor(finalConfig);
}
// 建立 AbortController 用於逾時控制
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
// 建立請求 URL
const url = new URL(finalConfig.url, this.config.baseURL);
if (finalConfig.params) {
Object.entries(finalConfig.params).forEach(([key, value]) => {
url.searchParams.append(key, String(value));
});
}
// 發送請求
const response = await fetch(url.toString(), {
method: finalConfig.method,
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: finalConfig.data ? JSON.stringify(finalConfig.data) : undefined,
signal: controller.signal,
});
clearTimeout(timeoutId);
// 處理錯誤回應
if (!response.ok) {
throw new ApiError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
await response.text()
);
}
// 解析回應
let data = await response.json();
// 套用回應攔截器
for (const interceptor of this.responseInterceptors) {
data = interceptor(data);
}
return data;
} catch (error) {
// 重試邏輯
if (attempt < this.config.retryAttempts && this.shouldRetry(error)) {
console.warn(`Request failed, retrying (${attempt}/${this.config.retryAttempts})...`);
await this.delay(Math.pow(2, attempt) * 1000); // 指數退避
return this.executeRequest(config, attempt + 1);
}
throw error;
}
}
/**
* 判斷是否應該重試
*/
private shouldRetry(error: any): boolean {
// 網路錯誤或 5xx 錯誤才重試
if (error.name === 'AbortError') return false;
if (error instanceof ApiError) {
return error.status >= 500;
}
return true;
}
/**
* 延遲工具函式
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 發送請求(公開方法)
*/
async request<T = any>(config: RequestConfig): Promise<T> {
// GET 請求檢查快取
if (config.method === 'GET' && config.cache !== false) {
const cacheKey = this.getCacheKey(config);
const cached = this.getCache(cacheKey);
if (cached) {
console.log(`Cache hit: ${cacheKey}`);
return cached;
}
const data = await this.executeRequest(config);
this.setCache(cacheKey, data);
return data;
}
return this.executeRequest(config);
}
/**
* GET 請求
*/
get<T = any>(url: string, params?: Record<string, any>, cache = true): Promise<T> {
return this.request<T>({ method: 'GET', url, params, cache });
}
/**
* POST 請求
*/
post<T = any>(url: string, data?: any): Promise<T> {
return this.request<T>({ method: 'POST', url, data });
}
/**
* PUT 請求
*/
put<T = any>(url: string, data?: any): Promise<T> {
return this.request<T>({ method: 'PUT', url, data });
}
/**
* DELETE 請求
*/
delete<T = any>(url: string): Promise<T> {
return this.request<T>({ method: 'DELETE', url });
}
/**
* 清除快取
*/
clearCache(pattern?: string): void {
if (!pattern) {
this.cache.clear();
return;
}
const regex = new RegExp(pattern);
for (const [key] of this.cache) {
if (regex.test(key)) {
this.cache.delete(key);
}
}
}
}
/**
* 自訂錯誤類別
*/
class ApiError extends Error {
constructor(
message: string,
public status: number,
public responseText: string
) {
super(message);
this.name = 'ApiError';
}
}
// 使用範例
const api = new ApiClient({
baseURL: 'https://api.example.com',
timeout: 15000,
retryAttempts: 3,
cacheTime: 10 * 60 * 1000, // 10 分鐘
});
// 添加認證攔截器
api.addRequestInterceptor((config) => {
const token = localStorage.getItem('auth_token');
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`,
};
}
return config;
});
// 添加錯誤處理攔截器
api.addResponseInterceptor((response) => {
if (response.code !== 0) {
throw new ApiError(
response.message || 'Unknown error',
response.code,
JSON.stringify(response)
);
}
return response.data;
});
// 實際使用
async function fetchUserProfile(userId: string) {
try {
const user = await api.get(`/users/${userId}`);
console.log('User profile:', user);
return user;
} catch (error) {
if (error instanceof ApiError) {
console.error(`API Error ${error.status}:`, error.message);
// 根據錯誤碼做不同處理
if (error.status === 401) {
// 重新登入
} else if (error.status === 404) {
// 顯示使用者不存在
}
} else {
console.error('Network error:', error);
}
}
}
現在讓我們實作一個輕量級的 GraphQL 客戶端:
/**
* 輕量級 GraphQL 客戶端
* 特性:型別安全、自動持久化查詢、批次請求
*/
interface GraphQLClientConfig {
endpoint: string;
headers?: Record<string, string>;
persistedQueries?: boolean;
batchInterval?: number;
}
interface GraphQLRequest {
query: string;
variables?: Record<string, any>;
operationName?: string;
}
interface GraphQLResponse<T = any> {
data?: T;
errors?: Array<{
message: string;
locations?: Array<{ line: number; column: number }>;
path?: string[];
}>;
}
class GraphQLClient {
private config: Required<GraphQLClientConfig>;
private queryHashCache: Map<string, string>;
private pendingRequests: Array<{
request: GraphQLRequest;
resolve: (value: any) => void;
reject: (error: any) => void;
}>;
private batchTimeout: NodeJS.Timeout | null;
constructor(config: GraphQLClientConfig) {
this.config = {
headers: {},
persistedQueries: false,
batchInterval: 10,
...config,
};
this.queryHashCache = new Map();
this.pendingRequests = [];
this.batchTimeout = null;
}
/**
* 計算查詢的 SHA256 雜湊(用於持久化查詢)
*/
private async hashQuery(query: string): Promise<string> {
if (this.queryHashCache.has(query)) {
return this.queryHashCache.get(query)!;
}
const encoder = new TextEncoder();
const data = encoder.encode(query);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
this.queryHashCache.set(query, hashHex);
return hashHex;
}
/**
* 發送單一請求
*/
private async sendRequest<T = any>(
request: GraphQLRequest
): Promise<GraphQLResponse<T>> {
const body: any = {
query: request.query,
variables: request.variables,
operationName: request.operationName,
};
// 持久化查詢
if (this.config.persistedQueries) {
const queryHash = await this.hashQuery(request.query);
body.extensions = {
persistedQuery: {
version: 1,
sha256Hash: queryHash,
},
};
delete body.query; // 第一次嘗試不發送完整查詢
}
try {
const response = await fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify(body),
});
const result: GraphQLResponse<T> = await response.json();
// 如果伺服器不支援持久化查詢,重新發送完整查詢
if (
this.config.persistedQueries &&
result.errors?.some(e => e.message.includes('PersistedQueryNotFound'))
) {
body.query = request.query;
const retryResponse = await fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify(body),
});
return retryResponse.json();
}
return result;
} catch (error) {
throw new GraphQLError('Network error', error);
}
}
/**
* 發送批次請求
*/
private async sendBatchRequests(): Promise<void> {
if (this.pendingRequests.length === 0) return;
const batch = this.pendingRequests.splice(0);
const requests = batch.map(({ request }) => request);
try {
const response = await fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...this.config.headers,
},
body: JSON.stringify(requests),
});
const results: GraphQLResponse[] = await response.json();
// 分發結果
results.forEach((result, index) => {
if (result.errors && result.errors.length > 0) {
batch[index].reject(new GraphQLError('GraphQL errors', result.errors));
} else {
batch[index].resolve(result.data);
}
});
} catch (error) {
// 批次失敗,所有請求都拋出錯誤
batch.forEach(({ reject }) => {
reject(new GraphQLError('Batch request failed', error));
});
}
}
/**
* 排程批次請求
*/
private scheduleBatchRequest(): void {
if (this.batchTimeout) return;
this.batchTimeout = setTimeout(() => {
this.batchTimeout = null;
this.sendBatchRequests();
}, this.config.batchInterval);
}
/**
* 執行 GraphQL 請求
*/
async request<T = any>(
query: string,
variables?: Record<string, any>,
options: { batch?: boolean; operationName?: string } = {}
): Promise<T> {
const request: GraphQLRequest = {
query: query.trim(),
variables,
operationName: options.operationName,
};
// 批次請求
if (options.batch) {
return new Promise((resolve, reject) => {
this.pendingRequests.push({ request, resolve, reject });
this.scheduleBatchRequest();
});
}
// 單一請求
const response = await this.sendRequest<T>(request);
if (response.errors && response.errors.length > 0) {
throw new GraphQLError('GraphQL errors', response.errors);
}
return response.data as T;
}
/**
* 執行查詢
*/
query<T = any>(
query: string,
variables?: Record<string, any>,
options?: { batch?: boolean }
): Promise<T> {
return this.request<T>(query, variables, options);
}
/**
* 執行變更
*/
mutate<T = any>(
mutation: string,
variables?: Record<string, any>
): Promise<T> {
return this.request<T>(mutation, variables, { batch: false });
}
/**
* 訂閱(需要 WebSocket 支援)
*/
subscribe(
subscription: string,
variables?: Record<string, any>,
callback?: (data: any) => void
): () => void {
// WebSocket 實作(簡化版)
const wsEndpoint = this.config.endpoint.replace(/^http/, 'ws');
const ws = new WebSocket(wsEndpoint, 'graphql-ws');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'connection_init',
}));
ws.send(JSON.stringify({
type: 'start',
payload: {
query: subscription,
variables,
},
}));
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.type === 'data' && callback) {
callback(message.payload.data);
}
};
// 返回取消訂閱函式
return () => {
ws.send(JSON.stringify({ type: 'stop' }));
ws.close();
};
}
}
/**
* GraphQL 錯誤類別
*/
class GraphQLError extends Error {
constructor(message: string, public details: any) {
super(message);
this.name = 'GraphQLError';
}
}
// 使用範例
const graphql = new GraphQLClient({
endpoint: 'https://api.example.com/graphql',
persistedQueries: true,
batchInterval: 10,
});
// 型別定義
interface User {
id: string;
name: string;
email: string;
posts: Array<{
id: string;
title: string;
createdAt: string;
}>;
}
// 查詢範例
async function fetchUser(userId: string) {
const query = `
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
posts(limit: 10) {
id
title
createdAt
}
}
}
`;
try {
const data = await graphql.query<{ user: User }>(
query,
{ userId },
{ batch: true }
);
console.log('User data:', data.user);
return data.user;
} catch (error) {
if (error instanceof GraphQLError) {
console.error('GraphQL Error:', error.details);
} else {
console.error('Network Error:', error);
}
}
}
// 變更範例
async function updateUserProfile(userId: string, name: string) {
const mutation = `
mutation UpdateUser($userId: ID!, $name: String!) {
updateUser(id: $userId, input: { name: $name }) {
id
name
updatedAt
}
}
`;
const data = await graphql.mutate(mutation, { userId, name });
return data;
}
// 訂閱範例
function subscribeToNewPosts(userId: string) {
const subscription = `
subscription OnNewPost($userId: ID!) {
postCreated(userId: $userId) {
id
title
createdAt
}
}
`;
const unsubscribe = graphql.subscribe(
subscription,
{ userId },
(data) => {
console.log('New post:', data.postCreated);
// 更新 UI
}
);
// 需要時取消訂閱
return unsubscribe;
}
讓我們實作一個進階的快取管理系統:
/**
* 進階快取管理系統
* 特性:多層快取、自動失效、依賴追蹤
*/
interface CacheConfig {
ttl?: number; // 生存時間 (毫秒)
staleWhileRevalidate?: number; // 過期後仍可使用的時間
maxSize?: number; // 最大快取數量
storage?: 'memory' | 'localStorage' | 'indexedDB';
}
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
dependencies?: Set<string>;
tags?: Set<string>;
}
class CacheManager {
private memoryCache: Map<string, CacheEntry<any>>;
private defaultConfig: Required<CacheConfig>;
private accessCount: Map<string, number>;
constructor(config: CacheConfig = {}) {
this.memoryCache = new Map();
this.accessCount = new Map();
this.defaultConfig = {
ttl: 5 * 60 * 1000, // 5 分鐘
staleWhileRevalidate: 60 * 1000, // 1 分鐘
maxSize: 100,
storage: 'memory',
...config,
};
// 定期清理過期快取
setInterval(() => this.cleanup(), 60 * 1000);
}
/**
* 生成快取鍵值
*/
private generateKey(key: string | any[]): string {
if (typeof key === 'string') return key;
return JSON.stringify(key);
}
/**
* 檢查快取是否過期
*/
private isExpired(entry: CacheEntry<any>): boolean {
return Date.now() - entry.timestamp > entry.ttl;
}
/**
* 檢查快取是否在 stale-while-revalidate 期間內
*/
private isStale(entry: CacheEntry<any>): boolean {
const age = Date.now() - entry.timestamp;
return age > entry.ttl && age <= entry.ttl + this.defaultConfig.staleWhileRevalidate;
}
/**
* LRU 淘汰策略
*/
private evictLRU(): void {
if (this.memoryCache.size < this.defaultConfig.maxSize) return;
// 找出存取次數最少的項目
let minAccess = Infinity;
let lruKey: string | null = null;
for (const [key] of this.memoryCache) {
const access = this.accessCount.get(key) || 0;
if (access < minAccess) {
minAccess = access;
lruKey = key;
}
}
if (lruKey) {
this.memoryCache.delete(lruKey);
this.accessCount.delete(lruKey);
}
}
/**
* 設定快取
*/
set<T>(
key: string | any[],
data: T,
options: {
ttl?: number;
dependencies?: string[];
tags?: string[];
} = {}
): void {
const cacheKey = this.generateKey(key);
// 檢查容量並淘汰
this.evictLRU();
const entry: CacheEntry<T> = {
data,
timestamp: Date.now(),
ttl: options.ttl || this.defaultConfig.ttl,
dependencies: options.dependencies ? new Set(options.dependencies) : undefined,
tags: options.tags ? new Set(options.tags) : undefined,
};
this.memoryCache.set(cacheKey, entry);
this.accessCount.set(cacheKey, 0);
}
/**
* 取得快取
*/
get<T>(key: string | any[]): T | null {
const cacheKey = this.generateKey(key);
const entry = this.memoryCache.get(cacheKey);
if (!entry) return null;
// 更新存取計數
this.accessCount.set(cacheKey, (this.accessCount.get(cacheKey) || 0) + 1);
// 完全過期,返回 null
if (this.isExpired(entry) && !this.isStale(entry)) {
this.memoryCache.delete(cacheKey);
return null;
}
return entry.data as T;
}
/**
* 取得快取(含 stale-while-revalidate)
*/
async getOrFetch<T>(
key: string | any[],
fetcher: () => Promise<T>,
options?: {
ttl?: number;
dependencies?: string[];
tags?: string[];
}
): Promise<T> {
const cached = this.get<T>(key);
const cacheKey = this.generateKey(key);
const entry = this.memoryCache.get(cacheKey);
// 快取存在且未過期,直接返回
if (cached && entry && !this.isExpired(entry)) {
return cached;
}
// 快取過期但在 stale-while-revalidate 期間內
// 返回舊資料,背景更新
if (cached && entry && this.isStale(entry)) {
// 背景重新取得資料
fetcher()
.then(data => {
this.set(key, data, options);
})
.catch(err => {
console.error('Background revalidation failed:', err);
});
return cached;
}
// 快取不存在或完全過期,重新取得
const data = await fetcher();
this.set(key, data, options);
return data;
}
/**
* 使快取失效(根據鍵值)
*/
invalidate(key: string | any[]): void {
const cacheKey = this.generateKey(key);
this.memoryCache.delete(cacheKey);
this.accessCount.delete(cacheKey);
}
/**
* 使快取失效(根據標籤)
*/
invalidateByTag(tag: string): void {
for (const [key, entry] of this.memoryCache) {
if (entry.tags?.has(tag)) {
this.memoryCache.delete(key);
this.accessCount.delete(key);
}
}
}
/**
* 使快取失效(根據依賴)
*/
invalidateByDependency(dependency: string): void {
for (const [key, entry] of this.memoryCache) {
if (entry.dependencies?.has(dependency)) {
this.memoryCache.delete(key);
this.accessCount.delete(key);
}
}
}
/**
* 清理過期快取
*/
private cleanup(): void {
const now = Date.now();
for (const [key, entry] of this.memoryCache) {
const age = now - entry.timestamp;
const maxAge = entry.ttl + this.defaultConfig.staleWhileRevalidate;
if (age > maxAge) {
this.memoryCache.delete(key);
this.accessCount.delete(key);
}
}
}
/**
* 清空所有快取
*/
clear(): void {
this.memoryCache.clear();
this.accessCount.clear();
}
/**
* 取得快取統計資訊
*/
getStats() {
return {
size: this.memoryCache.size,
maxSize: this.defaultConfig.maxSize,
hitRate: this.calculateHitRate(),
entries: Array.from(this.memoryCache.entries()).map(([key, entry]) => ({
key,
age: Date.now() - entry.timestamp,
accessCount: this.accessCount.get(key) || 0,
})),
};
}
/**
* 計算快取命中率
*/
private calculateHitRate(): number {
const totalAccess = Array.from(this.accessCount.values()).reduce((a, b) => a + b, 0);
const uniqueKeys = this.accessCount.size;
return uniqueKeys === 0 ? 0 : totalAccess / uniqueKeys;
}
}
// 整合到 API 客戶端
class CachedApiClient extends ApiClient {
private cacheManager: CacheManager;
constructor(config: ApiClientConfig, cacheConfig?: CacheConfig) {
super(config);
this.cacheManager = new CacheManager(cacheConfig);
}
/**
* 帶快取的 GET 請求
*/
async get<T = any>(
url: string,
params?: Record<string, any>,
options: {
cache?: boolean;
ttl?: number;
tags?: string[];
dependencies?: string[];
} = {}
): Promise<T> {
const cacheKey = ['GET', url, params];
if (options.cache === false) {
return super.get<T>(url, params, false);
}
return this.cacheManager.getOrFetch(
cacheKey,
() => super.get<T>(url, params, false),
{
ttl: options.ttl,
tags: options.tags,
dependencies: options.dependencies,
}
);
}
/**
* 變更操作後使快取失效
*/
async post<T = any>(
url: string,
data?: any,
options: {
invalidateTags?: string[];
invalidateDependencies?: string[];
} = {}
): Promise<T> {
const result = await super.post<T>(url, data);
// 使相關快取失效
if (options.invalidateTags) {
options.invalidateTags.forEach(tag => {
this.cacheManager.invalidateByTag(tag);
});
}
if (options.invalidateDependencies) {
options.invalidateDependencies.forEach(dep => {
this.cacheManager.invalidateByDependency(dep);
});
}
return result;
}
/**
* 取得快取統計
*/
getCacheStats() {
return this.cacheManager.getStats();
}
}
// 使用範例
const api = new CachedApiClient(
{
baseURL: 'https://api.example.com',
},
{
ttl: 5 * 60 * 1000,
staleWhileRevalidate: 60 * 1000,
maxSize: 200,
}
);
// 使用快取和標籤
async function fetchProducts() {
return api.get('/products', undefined, {
cache: true,
ttl: 10 * 60 * 1000,
tags: ['products'],
dependencies: ['catalog'],
});
}
// 新增產品後使相關快取失效
async function createProduct(productData: any) {
return api.post('/products', productData, {
invalidateTags: ['products'],
invalidateDependencies: ['catalog'],
});
}
// 檢視快取效能
console.log('Cache stats:', api.getCacheStats());
核心概念: API 設計直接影響前端開發效率和使用者體驗。RESTful 和 GraphQL 各有優勢,需要根據專案特性選擇合適方案。
關鍵技術:
實踐要點:
✅ 推薦做法: 使用 TypeScript 定義 API 回應型別,確保型別安全
✅ 推薦做法: 實作完整的錯誤處理機制,包括重試、逾時和錯誤分類
✅ 推薦做法: 使用 stale-while-revalidate 策略平衡即時性和效能
✅ 推薦做法: 為不同類型的資料設定合理的快取時間(使用者資料 5分鐘,靜態資料 1小時)
❌ 避免陷阱: 不要過度快取動態資料,避免使用者看到過期內容
❌ 避免陷阱: 不要忽略快取容量控制,避免記憶體洩漏
❌ 避免陷阱: 變更操作後不要忘記使相關快取失效
❌ 避免陷阱: 不要在錯誤處理中暴露敏感資訊給使用者
架構選擇: 你的專案更適合 RESTful 還是 GraphQL?考慮團隊技術棧、資料關聯複雜度和客戶端多樣性。
效能最佳化: 如何設計一個既能減少請求次數,又能保持資料即時性的快取策略?
實踐挑戰: 嘗試在現有專案中實作請求批次處理和智能快取,測量實際效能提升。